namespace ScrumSprintMonitor.ServerAdapter.TFS
{
	#region #using Directives

	using System;
	using System.Collections.Generic;
	using System.ComponentModel;
	using System.Diagnostics;
	using System.Linq;
	using System.Threading;

	using Infrastructure;

	using Isolation.TeamFoundationServer;

	#endregion

	public class TfsWorkItemStore : IWorkItemStore
	{
		#region Private Fields

		private readonly ILogger<TfsWorkItemStore> logger;
		private readonly IProcessTemplateAdapter processTemplateAdapter;
		private readonly SynchronizationContext syncContext;
		private readonly TfsQuery tfsQuery;
		private readonly ITfsWorkItemStore tfsWorkItemStore;
		private readonly Dictionary<int, IWorkItemAdapter> workItemDictionary = new Dictionary<int, IWorkItemAdapter>();
		private readonly BindingList<IWorkItemAdapter> workItemList = new BindingList<IWorkItemAdapter>();
		private ICancelableAsyncResult fetchWorkItemListAsyncResult;
		private bool raisedFirstUpdate;

		#endregion

		public TfsWorkItemStore(ITfsWorkItemStore tfsWorkItemStore, string tfsQueryString, IProcessTemplateAdapter processTemplateAdapter,
		                        ILogger<TfsWorkItemStore> logger)
		{
			this.tfsWorkItemStore = tfsWorkItemStore;
			this.processTemplateAdapter = processTemplateAdapter;
			this.tfsQuery = new TfsQuery(this.tfsWorkItemStore, tfsQueryString);
			this.logger = logger;
			this.workItemList.RaiseListChangedEvents = false;

			this.syncContext = SynchronizationContext.Current;
		}

		#region Private Methods

		private void RefreshWorkItemStore()
		{
			if (this.fetchWorkItemListAsyncResult != null && !this.fetchWorkItemListAsyncResult.IsCompleted)
			{
				return;
			}

			this.fetchWorkItemListAsyncResult = this.tfsQuery.BeginQuery(OnQueryResultsAvailable);
		}

		private void OnQueryResultsAvailable(IAsyncResult ar)
		{
			try
			{
				var workItems = this.tfsQuery.EndQuery(this.fetchWorkItemListAsyncResult);
				this.fetchWorkItemListAsyncResult = null;

				var newDictionary = new Dictionary<int, IWorkItemAdapter>(workItems.Count);
				foreach (var workItem in workItems)
				{
					var workItemAdapter = this.processTemplateAdapter.AdaptWorkItem(workItem);

					workItemAdapter.Initialize();

					newDictionary.Add(workItemAdapter.Id, workItemAdapter);
				}

				RaiseWorkItemsRefreshedAsync(newDictionary);

				PostAction(() => OnWorkItemListAvailable(newDictionary));
			}
			catch (TfsConnectionException ce)
			{
				this.logger.Info(ce, "Found exception while fetching work items.");

				RaiseWorkItemsRefreshedAsync(new ConnectionException(ce));
			}
			catch (Exception e)
			{
				this.logger.Info(e, "Found exception while fetching work items.");

				RaiseWorkItemsRefreshedAsync(e);
			}
		}

		private void RaiseWorkItemsRefreshedAsync(Dictionary<int, IWorkItemAdapter> workItemAdapters)
		{
			if (WorkItemsRefreshed != null)
			{
				PostAction(() => WorkItemsRefreshed(this, new RefreshResultsEventArgs(null)));
			}
		}

		private void RaiseWorkItemsRefreshedAsync(Exception e)
		{
			if (WorkItemsRefreshed != null)
			{
				PostAction(() => WorkItemsRefreshed(this, new RefreshResultsEventArgs(e)));
			}
		}

		private void OnWorkItemListAvailable(Dictionary<int, IWorkItemAdapter> newDictionary)
		{
			Debug.Assert(!Thread.CurrentThread.IsBackground);

			bool removed = RemoveExpiredItems(newDictionary);
			bool updated = UpdateExistingItems(newDictionary);
			bool added = AddNewItems(newDictionary);

			if (!this.raisedFirstUpdate || (removed || updated || added) && WorkItemsUpdated != null)
			{
				this.raisedFirstUpdate = true;

				WorkItemsUpdated(this, EventArgs.Empty);
			}
		}

		private bool AddNewItems(Dictionary<int, IWorkItemAdapter> newDictionary)
		{
			bool updated = false;
			IEnumerable<KeyValuePair<int, IWorkItemAdapter>> newItems = newDictionary;

			if (WorkItems.Any())
			{
				newItems = newDictionary.Where(adapter => !this.workItemDictionary.ContainsKey(adapter.Key));
			}

			foreach (var item in newItems)
			{
				WorkItems.Add(item.Value);
				this.workItemDictionary.Add(item.Key, item.Value);

				updated = true;
			}

			return updated;
		}

		private bool UpdateExistingItems(Dictionary<int, IWorkItemAdapter> newDictionary)
		{
			bool updated = false;

			if (WorkItems.Any())
			{
				foreach (var kvp in newDictionary)
				{
					IWorkItemAdapter existingWorkItemAdapter;

					if (this.workItemDictionary.TryGetValue(kvp.Key, out existingWorkItemAdapter))
					{
						if (!Equals(existingWorkItemAdapter, kvp.Value))
						{
							existingWorkItemAdapter.UpdateFrom(kvp.Value);

							updated = true;
						}
					}
				}
			}

			return updated;
		}

		private bool RemoveExpiredItems(IDictionary<int, IWorkItemAdapter> newDictionary)
		{
			bool updated = false;

			if (WorkItems.Any())
			{
				var expiredItemsList = new List<IWorkItemAdapter>();

				foreach (var workItemAdapter in WorkItems)
				{
					if (!newDictionary.ContainsKey(workItemAdapter.Id))
					{
						expiredItemsList.Add(workItemAdapter);
					}
				}

				if (expiredItemsList.Count != 0)
				{
					updated = true;

					foreach (var workItemAdapter in expiredItemsList)
					{
						WorkItems.Remove(workItemAdapter);
						this.workItemDictionary.Remove(workItemAdapter.Id);
					}
				}
			}

			return updated;
		}

		private void PostAction(Action action)
		{
			if (this.syncContext == null)
			{
				action();
			}
			else
			{
				this.syncContext.Post(state => action(), null);
			}
		}

		#endregion

		#region Implementation of IWorkItemStore

		public BindingList<IWorkItemAdapter> WorkItems
		{
			get { return this.workItemList; }
		}

		public event EventHandler<RefreshResultsEventArgs> WorkItemsRefreshed;
		public event EventHandler WorkItemsUpdated;

		public void Refresh()
		{
			RefreshWorkItemStore();
		}

		#endregion
	}
}